#targetengine "session"
#target indesign

/**
 * 选择一个 GREP 查找预设，并将匹配内容所在的框架应用指定对象样式。
 * - 仅作用于最近的父级框架（文本框架或图形框架）。
 * - 每个框架只会处理一次，避免重复应用。
 */
(function () {
    if (app.documents.length === 0) {
        alert("请先打开一个 InDesign 文档。");
        return;
    }

    var doc = app.activeDocument;
    var grepQueries = collectGrepQueries();
    if (grepQueries.length === 0) {
        alert("没有找到任何 GREP 查找预设。");
        return;
    }

    var styleEntries = collectObjectStyles(doc);
    if (styleEntries.length === 0) {
        alert("当前文档没有可用的对象样式。");
        return;
    }

    var ui = buildDialog(grepQueries, styleEntries);
    if (!ui) {
        return;
    }

    if (ui.win.show() !== 1) {
        ui.win.close();
        return;
    }

    var selectedQuery = grepQueries[ui.queryDropdown.selection.index];
    var selectedStyleEntry = styleEntries[ui.styleDropdown.selection.index];
    var clearOverrides = ui.clearOverrides.value;
    ui.win.close();

    app.doScript(function () {
        applyObjectStyleToMatches(doc, selectedQuery.name, selectedStyleEntry.style, clearOverrides);
    }, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "Apply Object Style to GREP Matches");
})();

function buildDialog(grepQueries, styleEntries) {
    var queryLabels = mapLabels(grepQueries);
    var styleLabels = mapLabels(styleEntries);

    var win = new Window("dialog", "对象样式应用到匹配框架");
    win.orientation = "column";
    win.alignChildren = ["fill", "top"];
    win.margins = 16;
    win.spacing = 12;

    var queryGroup = win.add("group");
    queryGroup.orientation = "column";
    queryGroup.alignChildren = ["fill", "center"];
    queryGroup.add("statictext", undefined, "选择 GREP 预设：");
    var queryDropdown = queryGroup.add("dropdownlist", undefined, queryLabels);
    queryDropdown.selection = 0;

    var styleGroup = win.add("group");
    styleGroup.orientation = "column";
    styleGroup.alignChildren = ["fill", "center"];
    styleGroup.add("statictext", undefined, "选择对象样式：");
    var styleDropdown = styleGroup.add("dropdownlist", undefined, styleLabels);
    styleDropdown.selection = 0;

    var clearOverrides = win.add("checkbox", undefined, "清除对象样式覆盖");
    clearOverrides.value = true;

    var buttonGroup = win.add("group");
    buttonGroup.alignment = ["right", "bottom"];
    buttonGroup.add("button", undefined, "确定", { name: "ok" });
    buttonGroup.add("button", undefined, "取消", { name: "cancel" });

    return {
        win: win,
        queryDropdown: queryDropdown,
        styleDropdown: styleDropdown,
        clearOverrides: clearOverrides
    };
}

function collectGrepQueries() {
    var results = [];
    var seenQueries = {};
    var seenFolders = {};
    var folderEntries = [];

    function registerFolder(folder, label) {
        if (!folder || !(folder instanceof Folder)) {
            return;
        }
        if (!folder.exists) {
            return;
        }
        var key = folder.fsName ? folder.fsName : folder.fullName;
        key = key.toLowerCase();
        if (seenFolders[key]) {
            return;
        }
        seenFolders[key] = true;
        folderEntries.push({ folder: folder, label: label });
    }

    var prefFolder = tryGetPreferencesFolder();
    if (prefFolder) {
        registerFolder(new Folder(prefFolder.fullName + "/Find-Change Queries/Grep"), "用户");
    }

    var userFolders = gatherUserGrepFolders();
    for (var u = 0; u < userFolders.length; u++) {
        registerFolder(userFolders[u], "用户");
    }

    var appFolders = gatherApplicationGrepFolders();
    for (var a = 0; a < appFolders.length; a++) {
        registerFolder(appFolders[a], "应用");
    }

    for (var i = 0; i < folderEntries.length; i++) {
        var entry = folderEntries[i];
        var files = [];
        try {
            files = entry.folder.getFiles("*.xml");
        } catch (e) {
            files = [];
        }
        for (var j = 0; j < files.length; j++) {
            var file = files[j];
            if (!(file instanceof File)) {
                continue;
            }
            var queryName = "";
            try {
                queryName = decodeURI(file.displayName.replace(/\.xml$/i, ""));
            } catch (err) {
                queryName = file.displayName.replace(/\.xml$/i, "");
            }
            if (!queryName || seenQueries[queryName]) {
                continue;
            }
            seenQueries[queryName] = true;
            results.push({
                name: queryName,
                label: (entry.label ? "[" + entry.label + "] " : "") + queryName
            });
        }
    }

    results.sort(function (a, b) {
        var aLabel = a.label.toLowerCase();
        var bLabel = b.label.toLowerCase();
        if (aLabel === bLabel) {
            return 0;
        }
        return aLabel > bLabel ? 1 : -1;
    });

    return results;
}

function collectObjectStyles(doc) {
    var list = [];
    recurseStyles(doc, "", list);
    list.sort(function (a, b) {
        return a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1;
    });
    return list;
}

function recurseStyles(container, prefix, store) {
    var styles = container.objectStyles;
    for (var i = 0; i < styles.length; i++) {
        var style = styles[i];
        var name = style.name;
        var label = prefix ? prefix + "/" + name : name;
        store.push({ style: style, label: label });
    }

    var groups = container.objectStyleGroups;
    for (var j = 0; j < groups.length; j++) {
        var group = groups[j];
        var nextPrefix = prefix ? prefix + "/" + group.name : group.name;
        recurseStyles(group, nextPrefix, store);
    }
}

function applyObjectStyleToMatches(doc, queryName, objectStyle, clearOverrides) {
    var originalFindPrefs = cloneProperties(app.findGrepPreferences);
    var originalChangePrefs = cloneProperties(app.changeGrepPreferences);

    try {
        app.findGrepPreferences = NothingEnum.NOTHING;
        app.changeGrepPreferences = NothingEnum.NOTHING;
        app.loadFindChangeQuery(queryName, SearchModes.GREP_SEARCH);

        var matches = doc.findGrep();
        if (!matches.length) {
            alert("没有匹配到任何内容。");
            return;
        }

        var anchoredLookup = createAnchoredLookup(doc);
        var processed = {};
        var appliedCount = 0;
        var skippedLocked = 0;
        var unresolved = 0;

        for (var i = 0; i < matches.length; i++) {
            var match = matches[i];
            if (!match || !match.isValid) {
                continue;
            }

            var frame = resolveContainerFrame(match, anchoredLookup);
            if (!frame || !frame.isValid) {
                unresolved++;
                continue;
            }

            if (frame.locked || (frame.itemLayer && frame.itemLayer.locked)) {
                skippedLocked++;
                continue;
            }

            var key = frame.id;
            if (processed[key]) {
                continue;
            }
            processed[key] = true;

            try {
                frame.applyObjectStyle(objectStyle, clearOverrides);
                appliedCount++;
            } catch (err) {
                skippedLocked++;
            }
        }

        var message = "找到 " + matches.length + " 个匹配项。\n已对 " + appliedCount + " 个框架应用对象样式 \"" + objectStyle.name + "\"。";
        if (skippedLocked) {
            message += "\n跳过 " + skippedLocked + " 个锁定或无法修改的框架。";
        }
        if (unresolved) {
            message += "\n未能定位到 " + unresolved + " 个匹配项的框架。";
        }
        alert(message);
    } catch (error) {
        alert("执行过程中出现错误：\n" + error.message);
    } finally {
        app.findGrepPreferences = NothingEnum.NOTHING;
        app.changeGrepPreferences = NothingEnum.NOTHING;
        restoreProperties(app.findGrepPreferences, originalFindPrefs);
        restoreProperties(app.changeGrepPreferences, originalChangePrefs);
    }
}

function resolveContainerFrame(item, anchoredLookup) {
    if (!item || !item.isValid) {
        return null;
    }

    if (isStylableFrame(item)) {
        return item;
    }

    var anchored = tryGetAnchoredFrame(item, anchoredLookup);
    if (anchored) {
        return anchored;
    }

    var parentFrame = tryGetParentTextFrame(item);
    if (parentFrame) {
        return parentFrame;
    }

    var parent = item.parent;
    var safety = 0;
    while (parent && safety < 20) {
        if (isStylableFrame(parent)) {
            return parent;
        }
        anchored = tryGetAnchoredFrame(parent, anchoredLookup);
        if (anchored) {
            return anchored;
        }
        var fromText = tryGetParentTextFrame(parent);
        if (fromText) {
            return fromText;
        }
        parent = parent.parent;
        safety++;
    }

    return null;
}

function tryGetParentTextFrame(target) {
    if (target.parentTextFrames && target.parentTextFrames.length > 0) {
        return target.parentTextFrames[0];
    }
    return null;
}

function tryGetAnchoredFrame(target, anchoredLookup) {
    var character = null;

    if (target instanceof Character) {
        character = target;
    } else if (target.parent instanceof Character) {
        character = target.parent;
    }

    if (!character) {
        return null;
    }

    var contents = "";
    try {
        contents = character.contents;
    } catch (e) { }

    if (contents !== SpecialCharacters.ANCHOR_MARKER && contents !== "\uFFFC") {
        return null;
    }

    var lookupKey;
    try {
        lookupKey = character.id;
    } catch (e2) {
        lookupKey = null;
    }

    if (lookupKey && anchoredLookup && anchoredLookup[lookupKey]) {
        return anchoredLookup[lookupKey];
    }

    return null;
}

function isStylableFrame(item) {
    return item instanceof TextFrame ||
        item instanceof Rectangle ||
        item instanceof Polygon ||
        item instanceof Oval;
}

function mapLabels(entries) {
    var labels = [];
    for (var i = 0; i < entries.length; i++) {
        labels.push(entries[i].label);
    }
    return labels;
}

function gatherApplicationGrepFolders() {
    var folders = [];
    var appFolder = getApplicationFolder();
    if (!appFolder) {
        return folders;
    }
    addFolderIfExists(folders, new Folder(appFolder.fullName + "/Presets/Find-Change Queries/Grep"));
    addFolderIfExists(folders, new Folder(appFolder.fullName + "/Find-Change Queries/Grep"));
    addFolderIfExists(folders, new Folder(appFolder.fullName + "/Resources/Find-Change Queries/Grep"));
    return folders;
}

function gatherUserGrepFolders() {
    var folders = [];
    var bases = [];
    var userData = Folder.userData;
    var appData = Folder.appData;

    addFolderIfExists(bases, safeFolder(userData, "Adobe/InDesign"));
    addFolderIfExists(bases, safeFolder(userData, "Adobe InDesign"));
    addFolderIfExists(bases, safeFolder(appData, "Adobe/InDesign"));
    addFolderIfExists(bases, safeFolder(appData, "Adobe InDesign"));

    for (var i = 0; i < bases.length; i++) {
        collectGrepFoldersRecursive(bases[i], 3, folders);
    }

    return folders;
}

function collectGrepFoldersRecursive(folder, depth, store) {
    if (!folder || !folder.exists || depth < 0) {
        return;
    }

    var direct = new Folder(folder.fullName + "/Find-Change Queries/Grep");
    addFolderIfExists(store, direct);

    if (depth === 0) {
        return;
    }

    var subFolders = getSubFolders(folder);
    for (var i = 0; i < subFolders.length; i++) {
        collectGrepFoldersRecursive(subFolders[i], depth - 1, store);
    }
}

function getSubFolders(folder) {
    var result = [];
    try {
        var entries = folder.getFiles(function (f) { return f instanceof Folder; });
        for (var i = 0; i < entries.length; i++) {
            result.push(entries[i]);
        }
    } catch (e) { }
    return result;
}

function addFolderIfExists(store, folder) {
    if (!folder || !(folder instanceof Folder)) {
        return;
    }
    if (!folder.exists) {
        return;
    }
    store.push(folder);
}

function getApplicationFolder() {
    try {
        var path = app.filePath;
        if (!path) {
            return null;
        }
        if (path instanceof Folder) {
            return path;
        }
        if (path instanceof File) {
            return path.parent;
        }
    } catch (e) { }
    return null;
}

function safeFolder(base, relativePath) {
    if (!base || !base.exists) {
        return null;
    }
    return new Folder(base.fullName + "/" + relativePath);
}

function cloneProperties(target) {
    var result = {};
    try {
        var props = target.properties;
        for (var key in props) {
            if (props.hasOwnProperty(key)) {
                result[key] = props[key];
            }
        }
    } catch (e) { }
    return result;
}

function restoreProperties(target, props) {
    if (!props) {
        return;
    }
    try {
        target.properties = props;
    } catch (e) { }
}

function tryGetPreferencesFolder() {
    try {
        if ("preferencesFolder" in app) {
            var folder = app.preferencesFolder;
            if (folder && folder.exists) {
                return folder;
            }
        }
    } catch (e) { }
    return null;
}

function createAnchoredLookup(doc) {
    var lookup = {};
    var items;
    try {
        items = doc.allPageItems;
    } catch (e) {
        items = [];
    }

    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        if (!item || !item.isValid) {
            continue;
        }
        var parent = item.parent;
        if (!(parent instanceof Character)) {
            continue;
        }

        var frame = null;
        if (isStylableFrame(item)) {
            frame = item;
        } else {
            frame = findStylableAncestor(item);
        }

        if (frame && frame.isValid) {
            try {
                lookup[parent.id] = frame;
            } catch (e2) { }
        }
    }

    return lookup;
}

function findStylableAncestor(item) {
    var parent = item.parent;
    var depth = 0;
    while (parent && depth < 10) {
        if (isStylableFrame(parent)) {
            return parent;
        }
        parent = parent.parent;
        depth++;
    }
    return null;
}
